import java.awt.* ;
import java.awt.event.* ;
import javax.swing.* ;
import javax.swing.event.* ;
import javax.imageio.* ;
import java.util.LinkedList ;
import java.io.File ;
import java.awt.image.BufferedImage ;
import java.io.IOException ;
import java.awt.print.* ;

public class Graph extends JPanel implements ActionListener , Printable {
    private static final long serialVersionUID = 1L;
    private static final JFileChooser fileChooser = new JFileChooser( ) ;
    private static PageFormat pageFormat = null ;
    private LinkedList<Curve> curves = new LinkedList<Curve> ( ) ;
    private LinkedList<PointPlot> pts = new LinkedList<PointPlot> ( ) ;
    private double xmin = -10 , xmax = 10 , ymin = -10 , ymax = 10 ;
    private WindowAdapter windowAdapter = null ;
    private	JFrame window = null ;
    private double minXLabel = -10 , minYLabel = -10 ;
    private double maxXLabel = 10 , maxYLabel = 10 ;
    private double deltaXLabel = 2 , deltaYLabel = 2 ;
    private boolean showXAxis = false ;
    private boolean showYAxis = false ;
    private boolean showXLabel = false ;
    private boolean showYLabel = false ;
    private boolean showTitle = false ;
    private String xLabel = null , yLabel = null , title = null ;
    public int SAVE_WIDTH = 2048 , SAVE_HEIGHT = 1024 ;
    
    // translates graph coordinates to integer grid coordinates, scaled to fit
    // in the rectangle defined by (x1,y1) and (x2,y2)
    // TODO:  Allow support for nonlinear transformations, e.g. logarithmic graphs,
    //        semilog, polar, etc...
    // NOTE:  Graphics objects are inherently short-lived... do not keep an instance
    //        of this class around past g's expiration
    private class SmartGraphics {
        private int x0 , y0 ;
        private int width , height ;
        private Graphics g ;
        
        public SmartGraphics( Graphics g , int x1 , int y1 , int x2 , int y2 ) {
            this.g = g ;
            x0 = Math.min( x1 , x2 ) ;
            y0 = Math.min( y1 , y2 ) ;
            width = Math.abs( x1 - x2 ) ;
            height = Math.abs( y1 - y2 ) ;
        }
        
        public int realX( double x ) {
            return ( int ) Math.round( ( x - xmin ) / ( xmax - xmin ) * width ) + x0 ;
        }
        
        private int realY( double y ) {
            return ( int ) Math.round( ( 1 - ( y - ymin ) / ( ymax - ymin ) ) * height ) + y0 ;
        }
        
        public void drawLine( OrderedPair p1 , OrderedPair p2 ) {
            Shape clip = g.getClip( ) ;
            g.setClip( x0 , y0 , width , height ) ;
            g.drawLine( realX( p1.x ) , realY( p1.y ) , realX( p2.x ) , realY( p2.y ) ) ;
            g.setClip( clip ) ;
        }
        
        // draws a cross centered at p
        public void drawCross( OrderedPair p , int radius ) {
            Shape clip = g.getClip( ) ;
            int x = realX( p.x ) ;
            int y = realY( p.y ) ;
            g.setClip( x0 , y0 , width , height ) ;
            g.drawLine( x - radius , y , x + radius , y ) ;
            g.drawLine( x , y - radius , x , y + radius ) ;
            g.setClip( clip ) ;
        }
        
        // draws a circle centered at p of radius radius
        public void drawCircle( OrderedPair p , int radius ) {
            Shape clip = g.getClip( ) ;
            g.setClip( x0 , y0 , width , height ) ;
            g.drawOval( realX( p.x ) - radius , realY( p.y ) - radius , 2 * radius , 2 * radius ) ;
            g.setClip( clip ) ;
        }
        
        // draw a filled square centered at p with sides 2 * radius
        public void drawFilledSquare( OrderedPair p , int radius ) {
            Shape clip = g.getClip( ) ;
            g.setClip( x0 , y0 , width , height ) ;
            g.fillRect( realX( p.x ) - radius , realY( p.y ) - radius , 2 * radius , 2 * radius ) ;
            g.setClip( clip ) ;
        }
        
        public void setColor( Color c ) {
            g.setColor( c ) ;
        }
    }
    
    public void setTitle( String title ) {
        this.title = title ;
        refresh( ) ;
    }
    
    public void showXAxis( boolean show ) {
        showXAxis = show ;
        refresh( ) ;
    }
    
    public void setXAxis( double x1 , double x2 , double deltax ) {
        minXLabel = Math.min( x1 , x2 ) ;
        maxXLabel = Math.max( x1 , x2 ) ;
        deltaXLabel = deltax ;
        refresh( ) ;
    }
    
    public void setYAxis( double y1 , double y2 , double deltay ) {
        minYLabel = Math.min( y1 , y2 ) ;
        maxYLabel = Math.max( y1 , y2 ) ;
        deltaYLabel = deltay ;
        refresh( ) ;
    }
    
    public void showYAxis( boolean show ) {
        showYAxis = show ;
        refresh( ) ;
    }
    
    public void showXAxis( ) {
        showXAxis( true ) ;
    }
    
    public void showYAxis( ) {
        showYAxis( true ) ;
    }
    
    public void setXLabel( String s ) {
        xLabel = s ;
        refresh( ) ;
    }
    
    public void setYLabel( String s ) {
        yLabel = s ;
        refresh( ) ;
    }
    
    public double getXMin( ) {
        return xmin ;
    }
    
    public double getYMin( ) {
        return ymin ;
    }
    
    public double getXMax( ) {
        return xmax ;
    }
    
    public double getYMax( ) {
        return ymax ;
    }
    
    public void refresh( ) {
        Graphics g = getGraphics( ) ;
        if ( g != null ) {
            paint( g ) ;
            g.dispose( ) ;
        }
    }
    
    public void paint( Graphics g ) {
        super.paint( g ) ;
        renderGraph( g ) ;
    }
    
    
    private void renderGraph( Graphics g ) {
        if ( g == null ) return ;
        renderGraph( g , getWidth( ) , getHeight( ) ) ;
    }
    
    private void centerText( Graphics g , String s , int x , int y , Font font ) {
        FontMetrics metrics ;
        
        if ( g == null ) return ;
        metrics = g.getFontMetrics( font ) ;
        g.setFont( font ) ;
        g.drawString( s , ( int ) ( x - metrics.stringWidth( s ) * .5 ) , y ) ;
    }
    
    private void leftText( Graphics g , String s , int x , int y , Font font ) {
        FontMetrics metrics ;
        
        if ( g == null ) return ;
        metrics = g.getFontMetrics( font ) ;
        g.setFont( font ) ;
        g.drawString( s , x , y ) ;
    }
    
    private void rightText( Graphics g , String s , int x , int y , Font font ) {
        FontMetrics metrics ;
        
        if ( g == null ) return ;
        metrics = g.getFontMetrics( font ) ;
        g.setFont( font ) ;
        g.drawString( s , ( int ) ( x - metrics.stringWidth( s ) ) , y ) ;
    }
    
    
    
    public void renderGraph( Graphics g , int width , int height ) {
        Font font = new Font( "SansSerif" , Font.PLAIN , ( int ) Math.min( 48 , Math.max( 10 , width * .02 ) ) ) ;
        FontMetrics metrics ;
        int textHeight , textWidth , x1 , y1 , x2 , y2 ;
        int spaceForLeftLabel = 0 , spaceForBottomLabel = 0 ;
        int i , radius = Math.max( width / 300  , 2 );
        double x , y ;
        SmartGraphics g2 ;
        
        if ( g == null ) return ;
        
        // setup fonts, etc...
        metrics = g.getFontMetrics( font ) ;
        textHeight = 2 + metrics.getHeight( ) ;
        g.setColor( Color.white ) ;
        g.fillRect( 0 , 0 , width , height ) ;
        g.setColor( Color.BLACK ) ;
        g.setFont( font ) ;
        
        // compute space needed for labels
        if ( showXAxis ) {
            spaceForBottomLabel = textHeight + 2 ;
        }
        if ( showYAxis ) {
            for ( y = minYLabel ; y <= maxYLabel ; y += deltaYLabel ) {
                spaceForLeftLabel = Math.max( spaceForLeftLabel , metrics.stringWidth( "" + y ) + 2 ) ;
            }
        }
        
        // setup coordinate translation
        x1 = spaceForLeftLabel + 2 ;
        y1 = 2 ;
        x2 = width - 1 ;
        y2 = height - spaceForBottomLabel - 2 ;
        g2 = new SmartGraphics( g , x1 , y1 , x2 , y2 ) ;
        
        // labels along the bottom
        if ( showXAxis ) {
            for ( x = minXLabel ; x <= maxXLabel ; x += deltaXLabel ) {
                centerText( g , "" + x , g2.realX( x ) , height - 2 , font ) ;
            }
        }
        
        // labels on the left
        if ( showYAxis ) {
            for ( y = minYLabel ; y <= maxYLabel ; y += deltaYLabel ) {
                rightText( g , "" + y , spaceForLeftLabel , ( int ) ( g2.realY( y ) + .5 * textHeight ) , font ) ;
            }
        }
        
        // draw a box around the graph area
        g.drawRect( x1 , y1 , x2 - x1 , y2 - y1 ) ;
        
        // plot the curves
        for ( Curve c : curves ) {
            if ( c.isVisible ) {
                g2.setColor( c.getColor( ) ) ;
                for ( i = 0 ; i < c.pts.length - 1 ; i ++ ) {
                    g2.drawLine( c.pts [ i ] , c.pts [ i + 1 ] ) ;
                }
            }
        }
        
        // plot the data points
        for ( PointPlot p : pts ) {
            if ( p.isVisible ) {
                g2.setColor( p.getColor( ) ) ;
                for ( PointPlot.Data d : p.pts ) {
                    if ( d.s == PointPlot.CIRCLE ) {
                        g2.drawCircle( d.pt , radius ) ;
                    } else if ( d.s == PointPlot.CROSS ) {
                        g2.drawCross( d.pt , radius ) ;
                    } else {
                        g2.drawFilledSquare( d.pt , radius ) ;
                    }
                }
            }
        }
        
    }
    
    
    public void printGraph( ) {
        PrinterJob job = PrinterJob.getPrinterJob( ) ;
        if ( job.printDialog( ) ) {
            try {
                if ( pageFormat == null ) {
                    pageFormat = new PageFormat( ) ;
                    pageFormat.setOrientation( PageFormat.LANDSCAPE ) ;
                }
                job.setPrintable( this , pageFormat ) ;
                job.print( ) ;
            } catch ( PrinterException e ) {
                JOptionPane.showMessageDialog( window , "Printing failed." , "Error" , JOptionPane.ERROR_MESSAGE ) ;
            }
        }
    }
    
    public int print( Graphics g , PageFormat pf , int page ) throws PrinterException {
        Graphics2D h = ( Graphics2D ) g ;
        int width , height ;
        
        if ( page > 0 ) {
            return NO_SUCH_PAGE ;
        }
        
        h.translate( pf.getImageableX( ) , pf.getImageableY( ) ) ;
        width = ( int ) pf.getImageableWidth( ) ;
        height = ( int ) pf.getImageableHeight( ) ;
        renderGraph( h , width , height ) ;
        
        return PAGE_EXISTS ;
    }
    
    public void save( File file , int width , int height ) throws IOException {
        BufferedImage image = new BufferedImage( width , height , BufferedImage.TYPE_INT_RGB ) ;
        Graphics2D g = image.createGraphics( ) ;
        renderGraph( g , width , height ) ;
        g.dispose( ) ;
        ImageIO.write( image , "png" , file ) ;
    }
    
    public void saveGraph( ) {
        fileChooser.setDialogTitle( "Save As..." ) ;
        if ( fileChooser.showSaveDialog( window ) == JFileChooser.APPROVE_OPTION ) {
            try {
                save( fileChooser.getSelectedFile( ) , SAVE_WIDTH , SAVE_HEIGHT ) ;
            } catch ( IOException e ) {
                JOptionPane.showMessageDialog( window , "Save failed." , "Error" , JOptionPane.ERROR_MESSAGE ) ;
            }
        }
    }
    
    private void viewWindow( ) {
        JOptionPane.showMessageDialog( window , "[" + xmin + "," + xmax
                + "] x [" + ymin + "," + ymax + "]" ,
                "View Window" , JOptionPane.PLAIN_MESSAGE ) ;
    }
    
    
    public void setWindow( double x1 , double y1 , double x2 , double y2 ) {
        xmin = Math.min( x1 , x2 ) ;
        xmax = Math.max( x1 , x2 ) ;
        ymin = Math.min( y1 , y2 ) ;
        ymax = Math.max( y1 , y2 ) ;
        refresh( ) ;
    }
    
    public void autoLabel( ) {
        double xsize = xmax - xmin ;
        double ysize = ymax - ymin ;
        double deltaX , x0 , y0 , deltaY ;
        if ( xsize <= 1 ) {
            deltaX = xsize * .25 ;
            x0 = xmin ;
        } else if ( xsize <= 2 ) {
            deltaX = .5 ;
            x0 = .5 * Math.round( 2 * xmin ) ;
        } else if ( xsize <= 5 ) {
            deltaX = 1 ;
            x0 = Math.round( xmin ) ;
        } else if ( xsize <= 10 ) {
            deltaX = 2 ;
            x0 = 2 * Math.round( xmin / 2 ) ;
        } else if ( xsize <= 20 ) {
            deltaX = 5 ;
            x0 = 5 * Math.round( xmin / 5 ) ;
        } else if ( xsize <= 50 ) {
            deltaX = 10 ;
            x0 = 10 * Math.round( xmin / 10 ) ;
        } else if ( xsize <= 100 ) {
            deltaX = 20 ;
            x0 = 20 * Math.round( xmin / 20 ) ;
        } else if ( xsize <= 200 ) {
            deltaX = 50 ;
            x0 = 50 * Math.round( xmin / 50 ) ;
        } else if ( xsize <= 500 ) {
            deltaX = 100 ;
            x0 = 100 * Math.round( xmin / 100 ) ;
        } else if ( xsize <= 1000 ) {
            deltaX = 200 ;
            x0 = 200 * Math.round( xmin / 200 ) ;
        } else if ( xsize <= 2000 ) {
            deltaX = 500 ;
            x0 = 500 * Math.round( xmin / 500 ) ;
        } else if ( xsize <= 5000 ) {
            deltaX = 1000 ;
            x0 = 1000 * Math.round( xmin / 1000 ) ;
        } else if ( xsize <= 10000 ) {
            deltaX = 2000 ;
            x0 = 2000 * Math.round( xmin / 2000 ) ;
        } else {
            deltaX = Math.round( xsize * .25 ) ;
            x0 = Math.round( xmin ) ;
        }
        if ( ysize < 1 ) {
            deltaY = ysize * .25 ;
            y0 = ymin ;
        } else if ( ysize <= 2 ) {
            deltaY = .5 ;
            y0 = .5 * Math.round( 2 * ymin ) ;
        } else if ( ysize <= 5 ) {
            deltaY = 1 ;
            y0 = Math.round( ymin ) ;
        } else if ( ysize <= 10 ) {
            deltaY = 2 ;
            y0 = 2 * Math.round( ymin / 2 ) ;
        } else if ( ysize <= 20 ) {
            deltaY = 5 ;
            y0 = 5 * Math.round( ymin / 5 ) ;
        } else if ( ysize <= 50 ) {
            deltaY = 10 ;
            y0 = 10 * Math.round( ymin / 10 ) ;
        } else if ( ysize <= 100 ) {
            deltaY = 20 ;
            y0 = 20 * Math.round( ymin / 20 ) ;
        } else if ( ysize <= 200 ) {
            deltaY = 50 ;
            y0 = 50 * Math.round( ymin / 50 ) ;
        } else if ( ysize <= 500 ) {
            deltaY = 100 ;
            y0 = 100 * Math.round( ymin / 100 ) ;
        } else if ( ysize <= 1000 ) {
            deltaY = 200 ;
            y0 = 200 * Math.round( ymin / 200 ) ;
        } else {
            deltaY = Math.round( ysize * .25 ) ;
            y0 = Math.round( ymin ) ;
        }
        showXAxis = true ;
        showYAxis = true ;
        setXAxis( x0 , xmax , deltaX ) ;
        setYAxis( y0 , ymax , deltaY ) ;
    }
    
    public void autoWindow( ) {
        double deltaY ;
        
        ymin = xmin = Double.POSITIVE_INFINITY ;
        xmax = ymax = Double.NEGATIVE_INFINITY ;
        
        for ( PointPlot p : pts ) {
            xmin = Math.min( p.getXMin( ) , xmin ) ;
            ymin = Math.min( p.getYMin( ) , ymin ) ;
            xmax = Math.max( p.getXMax( ) , xmax ) ;
            ymax = Math.max( p.getYMax( ) , ymax ) ;
        }
        
        for ( Curve c : curves ) {
            xmin = Math.min( c.xmin , xmin ) ;
            xmax = Math.max( c.xmax , xmax ) ;
            if ( c.ymin != Double.NEGATIVE_INFINITY ) {
                ymin = Math.min( c.ymin , ymin ) ;
            }
            if ( c.ymax != Double.POSITIVE_INFINITY ) {
                ymax = Math.max( c.ymax , ymax ) ;
            }
        }
        if ( ymax == Double.NEGATIVE_INFINITY || ymin == Double.POSITIVE_INFINITY ) {
            ymax = 1;
            ymin = -1;
        }
        deltaY = ymax - ymin ;
        ymin -= .05 * deltaY ;
        ymax += .05 * deltaY ;
        if ( ymin == ymax ) {
            ymin = ymin - 1 ;
            ymax = ymax + 1 ;
        }
        refresh( ) ;
    }
    
    // flips visibility of all curves with a matching object
    public void flipVisible( Object id ) {
        for ( Curve c : curves ) {
            if ( c.id == id ) {
                c.isVisible = ! c.isVisible ;
            }
        }
        for ( PointPlot p : pts ) {
            if ( p.id == id ) {
                p.isVisible = ! p.isVisible ;
            }
        }		refresh( ) ;
    }
    
    
    // sets to visible all curves with a matching object
    public void showCurve( Object id ) {
        for ( Curve c : curves ) {
            if ( c.id == id ) {
                c.isVisible = true ;
            }
        }
        for ( PointPlot p : pts ) {
            if ( p.id == id ) {
                p.isVisible = true ;
            }
        }
        refresh( ) ;
    }
    
    // hides all curves with a matching object
    public void hideCurve( Object id ) {
        for ( Curve c : curves ) {
            if ( c.id == id ) {
                c.isVisible = false ;
            }
        }
        for ( PointPlot p : pts ) {
            if ( p.id == id ) {
                p.isVisible = false ;
            }
        }
        refresh( ) ;
    }
    
    public void setCurveColor( Object id , Color col ) {
        for ( Curve c : curves ) {
            if ( c.id == id ) {
                c.setColor( col ) ;
            }
        }
        for ( PointPlot p : pts ) {
            if ( p.id == id ) {
                p.setColor( col ) ;
            }
        }
        refresh( ) ;
    }
    
    public boolean addCurve( double [ ] x , double [ ] y ) {
        return addCurve( x , y , null , true ) ;
    }
    
    public boolean addCurve( Curve c ) {
        if ( c == null ) return false ;
        curves.add( c ) ;
        refresh( ) ;
        return true ;
    }
    
    public boolean add( Curve c ) {
        return addCurve( c ) ;
    }
    
    public boolean add( PointPlot p ) {
        pts.add( p ) ;
        refresh( ) ;
        return true ;
    }
    
    public boolean addCurve( double [ ] x , double [ ] y , Object id , boolean visible ) {
        Curve c ;
        try {
            c = new Curve( x , y , id ) ;
            c.isVisible = visible ;
            curves.add( c ) ;
        } catch ( Exception ex ) {
            return false ;
        }
        refresh( ) ;
        return true ;
    }
    
    
    // provided for backwards compatibility
    public static final WindowAdapter QUIT_ON_CLOSE = new WindowAdapter( ) {
        public void windowClosing( WindowEvent e ) {
            e.getWindow().dispose( ) ;
            System.exit( 0 ) ;
        }
    } ;
    
    public static final WindowAdapter NO_EXIT_ON_CLOSE = new WindowAdapter( ) {
        public void windowClosing( WindowEvent e ) {
            e.getWindow().dispose( ) ;
        }
    } ;
    
    public void actionPerformed( ActionEvent e ) {
        String strMenuName ;
        strMenuName = e.getActionCommand( ) ;
        if ( strMenuName.equals( "Save" ) ) {
            saveGraph( ) ;
        } else if ( strMenuName.equals( "Close" ) ) {
            if ( window != null ) {
                window.dispose( ) ;
                if ( windowAdapter == QUIT_ON_CLOSE ) {
                    System.exit( 0 ) ;
                }
            }
        } else if ( strMenuName.equals( "Window" ) ) {
            viewWindow( ) ;
        } else if ( strMenuName.equals( "Exit" ) ) {
            System.exit( 0 ) ;
        } else if ( strMenuName.equals( "Print" ) ) {
            printGraph( ) ;
        }
    }
    
    public JFrame makeWindow( ) {
        return makeWindow( 640 , 480 ) ;
    }
    
    public JFrame makeWindow( int width , int height ) {
        return makeWindow( width , height , NO_EXIT_ON_CLOSE , "Graph"	) ;
    }
    
    public JFrame makeWindow( int width , int height , WindowAdapter winControls , String title ) {
        JPanel content = new JPanel( new BorderLayout( ) ) ;
        Menu fileMenu = new Menu( "File" ) , viewMenu = new Menu( "View" ) ;
        MenuItem item ;
        MenuBar menus = new MenuBar( ) ;
        boolean addExit = true ;
        
        window = new JFrame( ) ;
        windowAdapter = winControls ;
        
        item = new MenuItem( "Save", new MenuShortcut( KeyEvent.VK_S ) ) ;
        item.addActionListener( this ) ;
        fileMenu.add( item ) ;
        
        item = new MenuItem( "Close" , new MenuShortcut( KeyEvent.VK_W ) ) ;
        item.addActionListener( this ) ;
        fileMenu.add( item ) ;
        
        fileMenu.addSeparator( ) ;
        
        item = new MenuItem( "Print" , new MenuShortcut( KeyEvent.VK_P ) ) ;
        item.addActionListener( this ) ;
        fileMenu.add( item ) ;
        
        item = new MenuItem( "Window" ) ;
        item.addActionListener( this ) ;
        viewMenu.add( item ) ;
        
        
        // don't add an exit option on a mac; quit is already provided
        try {
            addExit = ! System.getProperty( "os.name" ) . equals( "Mac OS X" ) ;
        } catch ( Exception ex ) {
        }
        
        if ( addExit ) {
            fileMenu.addSeparator( ) ;
            
            item = new MenuItem( "Exit" , new MenuShortcut( KeyEvent.VK_Q ) ) ;
            item.addActionListener( this ) ;
            fileMenu.add( item ) ;
        }
        
        menus.add( fileMenu ) ;
        menus.add( viewMenu ) ;
        window.setMenuBar( menus ) ;
        
        window.addWindowListener( winControls ) ;
        
        content.add( this , BorderLayout.CENTER ) ;
        window.add( content ) ;
        
        window.setTitle( title ) ;
        window.setSize( width , height ) ;
        window.setVisible( true ) ;
        
        return window ;
    }
    
    public static void main( String [ ] args ) {
        final int NUMPTS = 1000 ;
        Graph g = new Graph( ) ;
        double x [ ] = new double [ NUMPTS ] ;
        double y [ ] = new double [ NUMPTS ] ;
        int i ;
        double dx = 6.28 / NUMPTS ;
        double x0 = -3.14 ;
        double temp ;
        Object idOfTan = new Object( ) ;
        
        for ( i = 0 ; i < NUMPTS ; i ++ ) {
            temp = x0 + dx * i ;
            x [ i ] = temp ;
            y [ i ] = Math.sin( temp ) ;
        }
        g.addCurve( x , y ) ;
        g.setWindow( -3.14 , -3 , 3.14 , 3 ) ;
        
        g.makeWindow( 640 , 480 , QUIT_ON_CLOSE , "Graph" ) ;
        g.showXAxis( ) ;
        g.showYAxis( ) ;
        
        y = new double [ NUMPTS ] ;
        for ( i = 0 ; i < NUMPTS ; i ++ ) {
            y [ i ] = Math.tan( x[i] ) ;
        }
        g.addCurve( x , y , idOfTan , false ) ;
        g.setCurveColor( idOfTan , Color.BLUE ) ;
        g.showCurve( idOfTan ) ;
    }
}
